/**
 * 
 */
package cs342.project1.mathNotebook.db;

import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

/** Superclass for database-mapped objects 
 * @author Aaron Heise
 *
 */
public abstract class DBObject {

	/**
	 * Keeps a list of weak references to all DBObjects instantiated for caching purposes.
	 */
	protected static List<WeakReference<DBObject>> loadedObjects = new LinkedList<WeakReference<DBObject>>();
	
	/** Extracts an array of live objects from the cache and prunes the dead ones.
	 * @param context match objects from context
	 * @return array of live DBObjects
	 */
	protected static DBObject[] getLoadedObjects(DatabaseContext context) {
		ArrayList<DBObject> liveObjectList = new ArrayList<DBObject>();
		for(Iterator<WeakReference<DBObject>> i = DBObject.loadedObjects.iterator(); i.hasNext();) {
			WeakReference<DBObject> r = i.next();
			DBObject o = r.get();
			
			if(o == null) {
				DBObject.loadedObjects.remove(o);
			} else if(o.context == context){
				liveObjectList.add(o);
			}
		}
		
		DBObject[] objs = new DBObject[liveObjectList.size()];
		
		for(int i = 0; i < liveObjectList.size(); i++) {
			objs[i] = liveObjectList.get(i);
		}
		
		return objs;
	}
	
	/**
	 * context to which this object belongs.
	 */
	protected DatabaseContext         context     = null;
	/**
	 * weak reference to this object for distributing
	 */
	protected WeakReference<DBObject> weakRef     = null;
	/**
	 * true if this object has been initialized from the database
	 */
	protected boolean                 initialized = false;
	
	/** Create a new DBObject.  Each subclass should call this constructor.
	 * @param context context this object belongs to
	 */
	protected DBObject(DatabaseContext context) {
		this.context = context;
		//this.initializeFromContext();
		this.weakRef = new WeakReference<DBObject>(this);
		DBObject.loadedObjects.add(this.getWeakReference());
	}
	
	/** checks if object has been initialized from the database.
	 * @return true if object has been initialized from the database
	 */
	protected boolean isInitialized() {
		return initialized;
	}
	
	/** Checks if object exists in database.  
	 * 
	 * Because of lazy loading, in some cases a DBObject
	 * can be instantiated without having an associated record in the database.<br><br>  
	 *  
	 *  When objects are created (INSERT) or instantiated from a relationship (SELECT), the database
	 *  constraints almost always prevent invalid objects from being loaded.  However, when objects
	 *  are manually created by name, there is not necessarily any check that the created object is
	 *  valid.  The object is not checked until it is initialized.<br><br>
	 *  
	 *  If an attempt is made to initialize an object that does not exist in the database, an
	 *  exception will be thrown.  Therefore, if an object is created manually (i.e. with 
	 *  DBPerson.getPersonByUsername()), this method is useful to verify the object actually exists.<br><br>
	 *  
	 *  A side effect of this method is that the object will be initialized, since a database call to
	 *  verify the object exists takes as long as a database call to initialize the object.
	 * @return true if object exists in database
	 */
	public boolean exists() {
		boolean loaded = false;
		try {
			this.initializeIfNeeded();
			loaded = true;
		} catch (DatabaseException e) {
			loaded = false;
		}
		return loaded;
	}
	
	/** If this object has not yet been initialized, initialize it.
	 * @throws DatabaseException if database cannot be connected or if object does not exist
	 */
	protected void initializeIfNeeded() throws DatabaseException {
		if(!this.isInitialized()) {
			this.initializeFromContext();
			this.initialized = true;
		}
	}
	
	/** 
	 * @return a weak reference to this object
	 */
	public WeakReference<DBObject> getWeakReference() {
		return this.weakRef;
	}
	
	/**
	 * invalidates this object's initialization.  The object will re-initialize itself when
	 * it is next used.
	 */
	public void invalidate() {
		this.initialized = false;
	}
	
	/** Initialize this object from the database.
	 * @throws DatabaseException if database cannot be connected or if object does not exist
	 */
	protected abstract void initializeFromContext() throws DatabaseException;
	
	/** Update or insert this instance into the database.
	 * @throws DatabaseException if database cannot be connected or if the object cannot be created
	 */
	protected abstract void writeToContext() throws DatabaseException;
}
